#include "util/process.h"

#include <assert.h>
#include <errno.h>
/* fcntl.h，是unix标准中通用的头文件，其中包含的相关函数有 open，fcntl，shutdown，unlink，fclose等 */
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include "util/log.h"

enum sc_process_result
sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags,
                     int *pin, int *pout, int *perr) {
    /*
    inherit 继承
    pin,pout,perr传入的有可能是NULL
    */
    // 继承标准输出                                
    bool inherit_stdout = !pout && !(flags & SC_PROCESS_NO_STDOUT);
    // 继承标准的错误输出    
    bool inherit_stderr = !perr && !(flags & SC_PROCESS_NO_STDERR);

    int in[2];
    int out[2];
    int err[2];
    int internal[2]; // communication between parent and children

/*
https://blog.csdn.net/weixin_44471948/article/details/120846877

管道也叫无名管道，它是UNIX系统IPC(进程间通信)的最古老形式，所有的UNIX系统都支持这种机制。
管道是一种把两个进程之间的标准输入和标准输出连接起来的机制，从而提供一种让多个进程间通信的方法，当进程创建管道时，每次
都需要提供两个文件描述符来操作管道。其中一个对管道进行写操作，另一个对管道进行读操作。对管道的读写与一般的IO系统函数一
致，使用write()函数写入数据，使用read()读出数据。

pipe函数用于创建一个管道，以实现进程间的通信。

int pipe(int filedes[2]);
返回值：成功，返回0，否则返回-1。参数数组包含pipe使用的两个文件的描述符。fd[0]:读管道，fd[1]:写管道。
*/

    /* 初始化几个管道 */
    if (pipe(internal) == -1) {
        // https://blog.csdn.net/zhengnianli/article/details/109324424

/*
perror(s) 用来将上一个函数发生错误的原因输出到标准设备(stderr)。
参数 s 所指的字符串会先打印出，后面再加上错误原因字符串。此错误原因依照全局变量errno的值来决定要输出的字符串。
在库函数中有个errno变量，每个errno值对应着以字符串表示的错误类型。
当你调用"某些"函数出错时，该函数已经重新设置了errno的值。perror函数只是将你输入的一些信息和errno所对应的错误一起输出。
*/        
        perror("pipe");
        return SC_PROCESS_ERROR_GENERIC;
    }

    // 打开标准输出管道
    if (pin) {
        // 如果定义了标准输入
        if (pipe(in) == -1) {
            perror("pipe-in init error");
            // close()函数用于关闭由open()函数所打开的文件
            close(internal[0]);
            close(internal[1]);
            return SC_PROCESS_ERROR_GENERIC;
        }
    }
    if (pout) {
        if (pipe(out) == -1) {
            perror("pipe-out init error");
            // clean up
            if (pin) {
                close(in[0]);
                close(in[1]);
            }
            close(internal[0]);
            close(internal[1]);
            return SC_PROCESS_ERROR_GENERIC;
        }
    }
    if (perr) {
        if (pipe(err) == -1) {
            perror("pipe-error init error");
            // clean up
            if (pout) {
                close(out[0]);
                close(out[1]);
            }
            if (pin) {
                close(in[0]);
                close(in[1]);
            }
            close(internal[0]);
            close(internal[1]);
            return SC_PROCESS_ERROR_GENERIC;
        }
    }

/*
进程会先调用pipe函数产生管道，接着调用fork()函数，
fork函数会将父进程的相关数据结构继承到子进程中，这样就使子进程中的文件描述符表中的fd[0]和fd[1]指向父进程所指向的管道文件，
这样就能实现两个进程之间的通信了

fork是复制进程的函数，程序一开始就会产生一个进程，当这个进程(代码)执行到fork()时，
fork就会复制一份原来的进程即就是创建一个新进程,我们称子进程，
而原来的进程我们称为父进程，此时父子进程是共存的，他们一起向下执行代码。

fork的返回值问题:

在父进程中，fork返回新创建子进程的进程ID；
在子进程中，fork返回0；
如果出现错误，fork返回一个负值；  

getppid():得到一个进程的父进程的PID;
getpid():得到当前进程的PID;

*注意:在fork函数执行完毕后，如果创建新进程成功，则出现两个进程，
一个是子进程，一个是父进程。
在子进程中，fork函数返回0，
在父进程中，fork返回新创建子进程的进程ID。
我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

注意的一点：就是调用fork函数之后，一定是两个进程同时执行fork函数之后的代码，而之前的代码以及由父进程执行完毕。
原文链接：https://blog.csdn.net/weixin_51609435/article/details/124849719
*/

    *pid = fork();
    if (*pid == -1) {
        perror("fork-pipe created error");
        // clean up
        if (perr) {
            close(err[0]);
            close(err[1]);
        }
        if (pout) {
            close(out[0]);
            close(out[1]);
        }
        if (pin) {
            close(in[0]);
            close(in[1]);
        }
        close(internal[0]);
        close(internal[1]);
        return SC_PROCESS_ERROR_GENERIC;
    }

    if (*pid == 0) {
        /* 这是fork后的子进程需要处理的事项 */
        if (pin) {
            // STDIN_FILENO 标准输入
            if (in[0] != STDIN_FILENO) {
              /*

                dup和dup2也是两个非常有用的调用，它们的作用都是用来复制一个文件的描述符。它们经
常用来重定向进程的stdin、stdout和stderr

这两个函数都可以来复制一个现有的文件描述符，他们的声明如下：

int dup(int fd);
int dup2(int fd, int fd2);

    关于 dup 函数，当我们调用它的时候，dup 会返回一个新的描述符，这个描述一定是当前可用文件描述符中的最小值。
我们知道，一般的 0、1、2 描述符分别被标准输入、输出、错误占用，所以在程序中如果 close 掉标准输出 1 后，调用 dup 函数，
此时返回的描述符就是 1 。
对于 dup2，可以用 fd2 指定新描述符的值，如果 fd2 本身已经打开了，则会先将其关闭。如果 fd 等于 fd2，则返回 fd2，并不关闭它。
这两个函数返回的描述符与 fd 描述符所指向的文件共享同一文件表项。
*/                
                dup2(in[0], STDIN_FILENO);
                close(in[0]);
            }
            close(in[1]);
        } else {
            int devnull = open("/dev/null", O_RDONLY | O_CREAT, 0666);
            if (devnull != -1) {
                dup2(devnull, STDIN_FILENO);
            } else {
                LOGE("Could not open /dev/null for stdin");
            }
        }

        if (pout) {
            // TODO 这里为什么是重定向了又关闭？
            if (out[1] != STDOUT_FILENO) {
                dup2(out[1], STDOUT_FILENO);
                close(out[1]);
            }
            close(out[0]);
        } else if (!inherit_stdout) {
            int devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666);
            if (devnull != -1) {
                dup2(devnull, STDOUT_FILENO);
            } else {
                LOGE("Could not open /dev/null for stdout");
            }
        }

        if (perr) {
            if (err[1] != STDERR_FILENO) {
                dup2(err[1], STDERR_FILENO);
                close(err[1]);
            }
            close(err[0]);
        } else if (!inherit_stderr) {
            int devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666);
            if (devnull != -1) {
                dup2(devnull, STDERR_FILENO);
            } else {
                LOGE("Could not open /dev/null for stderr");
            }
        }

        close(internal[0]);
        enum sc_process_result err;

       /*
        sigemptyset、sigaddset、sigprocmask的用法 信号未决，信号阻塞 信号的捕捉
        https://blog.csdn.net/u012736748/article/details/75040897
        */

        // Somehow SDL masks many signals - undo them for other processes
        // https://github.com/libsdl-org/SDL/blob/release-2.0.18/src/thread/pthread/SDL_systhread.c#L167
        sigset_t mask;

        // 函数初始化信号集合set,将set 设置为空.
        sigemptyset(&mask);

                /*
        sigprocmask 最为关键的一个函数.在使用之前要先设置好信号集合set
将指定的信号集合set 加入到进程的信号阻塞集合之中去
        */
        sigprocmask(SIG_SETMASK, &mask, NULL);

/*
fcntl() 针对（文件）描述符提供控制。

复制一个现有的描述符（cmd = F_DUPFD）。 
获得／设置文件描述符标记（cmd = F_GETFD 或 F_SETFD）。 
获得／设置文件状态标记（cmd = F_GETFL 或 F_SETFL）。 
获得／设置异步I/O所有权（cmd = F_GETOWN 或 F_SETOWN）。
获得／设置记录锁（cmd = F_GETLK、F_SETLK 或 F_SETLKW）。
————————————————
版权声明：本文为CSDN博主「Ruo_Xiao」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。
原文链接：https://blog.csdn.net/itworld123/article/details/124906308

*/
        if (fcntl(internal[1], F_SETFD, FD_CLOEXEC) == 0) {
            /*
            exec系列函数（execl,execlp,execle,execv,execvp)使用
            https://blog.csdn.net/sun172270102/article/details/109892085

            https://blog.csdn.net/cunchi4221/article/details/107470898

            execvp()会从环境变量所指的目录中查找符合参数 file 的文件名, 找到后执行该文件, 然后将第二个参数argv 传给该执行的文件。
            返回值：如果执行成功则函数不会返回, 执行失败则直接返回-1.
            */            
            execvp(argv[0], (char *const *) argv);
            perror("exec");
            err = errno == ENOENT ? SC_PROCESS_ERROR_MISSING_BINARY
                                  : SC_PROCESS_ERROR_GENERIC;
        } else {
            perror("fcntl");
            err = SC_PROCESS_ERROR_GENERIC;
        }
        // send err to the parent
        if (write(internal[1], &err, sizeof(err)) == -1) {
            perror("write");
        }
        close(internal[1]);

        /*
        exit()和_exit()的区别在于，exit()会首先将所有使用atexit注册的函数进行调用以后再推出，而_exit()则是直接结束程序。
注意：_cxa_atexit()和atexit()这两个函数的作用相同，可以看成等价。这两个函数的作用是注册一些函数用于在main()函数调用完以后再调用。

        此时子进程就直接退出了
        */        
        _exit(1);
    }

    // parent，判断当前处于父进程中
    assert(*pid > 0);

    close(internal[1]);

    enum sc_process_result res = SC_PROCESS_SUCCESS;
    // wait for EOF or receive err from child
    if (read(internal[0], &res, sizeof(res)) == -1) {
        perror("read");
        res = SC_PROCESS_ERROR_GENERIC;
    }

    close(internal[0]);

    if (pin) {
        // 写管道是1，读管道是0。注意下面的几个关闭函数传参
        close(in[0]);
        *pin = in[1];
    }
    if (pout) {
        *pout = out[0];
        // 关闭写管道
        close(out[1]);
    }
    if (perr) {
        *perr = err[0];
        close(err[1]);
    }

    // 父进程返回子进程的id号
    return res;
}

bool
sc_process_terminate(pid_t pid) {
    if (pid <= 0) {
        LOGE("Requested to kill %d, this is an error. Please report the bug.\n",
             (int) pid);
        /* 
        1、abort函数的作用是异常终止一个进程，意味着abort后面的代码将不再执行。
        2、当调用abort函数时，会导致程序异常终止，而不会进行一些常规的清除工作。
        */                
        abort();
    }

    /* kill函数：
    在linux里使用的Kill命令。实际上是对Kill()函数的一个包装;
    向进程或进程组发送一个（常用于关闭进程）信号 （成功返回 0； 否则，返回 -1 并且errno会被设置相应的值.）
    pid：接收信号的进程（组）的进程号
    pid>0：发送给进程号为pid的进程
    pid=0：发送给当前进程所属进程组里的所有进程
    pid=-1：发送给除1号进程和自身以外的所有进程
    pid<-1：发送给属于进程组-pid的所有进程

    SIGKILL 9 AEF Kill信号
    
    On success (at least one signal was sent), zero is returned.  
    On error, -1 is returned, and errno is set appropriately.
    */    
    return kill(pid, SIGKILL) != -1;
}

sc_exit_code
sc_process_wait(pid_t pid, bool close) {
    int code;
    
    // WEXITED ：等待终止的子进程，无论其是否正常返回。
    int options = WEXITED;
    if (!close) {
        // WNOWAIT：会返回子进程状态，但是子进程依然处于可等待状态，稍后可以再次等待并获取相同的信息。
        options |= WNOWAIT;
    }

    siginfo_t info;
    /*
    waitid提供了更加丰富的选项，使用waitid可以实现wait和waitpid的所有功能，
        该函数调用成功返回0，并且会将子进程的信息存储在infop中
    P_PID 是类型 等待进程ID等于id的进程终止；

    如果infop不为NULL，则档waitid成功返回时，这个结构将会被填写，我们可以从该结构中获取我们感兴趣的信息，其中有：

    si_pid: 子进程的进程id；
    si_uid:子进程的真实用户id；
    si_signo:总是设置为SIGCHID；
    si_status:可以是子进程的退出状态，或者是造成子进程状态改变的原因，si_code用来解释这个字段；
    si_code:可以是CLD_EXITED、CLD_KILLED、CLD_DUMPED、CLD_TRAPPED、CLD_CONTINUED，记录子进程状态改变的原因；
    
    options
    WEXITED 等待子进程被终止；
    WSTOPPED 等待子进程停止，stoped状态可以恢复运行；
    WCONTINUED 等待子进程从stoped状态恢复；
    WNOHANG 如果没有子进程立即返回；
    WNOWAIT 使子进程处于可等待状态，后续可以调用wait获取子进程的状态信息；
    */

    int r = waitid(P_PID, pid, &info, options);
    if (r == -1 || info.si_code != CLD_EXITED) {
        // could not wait, or exited unexpectedly, probably by a signal
        code = SC_EXIT_CODE_NONE;
    } else {
        code = info.si_status;
    }
    return code;
}

void
sc_process_close(pid_t pid) {
    sc_process_wait(pid, true); // ignore exit code
}

ssize_t
sc_pipe_read(int pipe, char *data, size_t len) {
    /*
    read()函数用来读取打开文件的数据
    ssize_t 是有符号整数类型；size_t是无符号整数类型
        fd：      文件描述符(文件名或文件路径)
        buf：     存放输入数据的内存缓冲区地址
        count：   指定最多能读取的字节数
    返回值
        读取成功：返回的是读取到的字节数
        读取到文件尾：0
        读取失败：-1        
    */    
    return read(pipe, data, len);
}

void
sc_pipe_close(int pipe) {
    /*
    功能描述：用于关闭一个被打开的的文件
    函数原型:int close(int fd)
    参数：fd文件描述符
    函数返回值：0成功，-1出错
    */    
    if (close(pipe)) {
        perror("close pipe");
    }
}
